msg_tool\scripts\bgi\archive/
v1.rs

1//! Buriko General Interpreter/Ethornell Archive File Version 1 (.arc)
2use super::bse::*;
3use super::dsc::*;
4use crate::ext::io::*;
5use crate::scripts::base::*;
6use crate::types::*;
7use crate::utils::encoding::encode_string;
8use crate::utils::struct_pack::*;
9use anyhow::Result;
10use msg_tool_macro::*;
11use std::collections::HashMap;
12use std::io::{Read, Seek, SeekFrom, Write};
13use std::sync::{Arc, Mutex};
14
15#[derive(Debug)]
16/// Builder for BGI Archive Version 1 scripts.
17pub struct BgiArchiveBuilder {}
18
19impl BgiArchiveBuilder {
20    /// Creates a new instance of `BgiArchiveBuilder`.
21    pub const fn new() -> Self {
22        BgiArchiveBuilder {}
23    }
24}
25
26impl ScriptBuilder for BgiArchiveBuilder {
27    fn default_encoding(&self) -> Encoding {
28        Encoding::Cp932
29    }
30
31    fn default_archive_encoding(&self) -> Option<Encoding> {
32        Some(Encoding::Cp932)
33    }
34
35    fn build_script(
36        &self,
37        data: Vec<u8>,
38        filename: &str,
39        _encoding: Encoding,
40        archive_encoding: Encoding,
41        config: &ExtraConfig,
42        _archive: Option<&Box<dyn Script>>,
43    ) -> Result<Box<dyn Script>> {
44        Ok(Box::new(BgiArchive::new(
45            MemReader::new(data),
46            archive_encoding,
47            config,
48            filename,
49        )?))
50    }
51
52    fn build_script_from_file(
53        &self,
54        filename: &str,
55        _encoding: Encoding,
56        archive_encoding: Encoding,
57        config: &ExtraConfig,
58        _archive: Option<&Box<dyn Script>>,
59    ) -> Result<Box<dyn Script>> {
60        if filename == "-" {
61            let data = crate::utils::files::read_file(filename)?;
62            Ok(Box::new(BgiArchive::new(
63                MemReader::new(data),
64                archive_encoding,
65                config,
66                filename,
67            )?))
68        } else {
69            let f = std::fs::File::open(filename)?;
70            let reader = std::io::BufReader::new(f);
71            Ok(Box::new(BgiArchive::new(
72                reader,
73                archive_encoding,
74                config,
75                filename,
76            )?))
77        }
78    }
79
80    fn build_script_from_reader(
81        &self,
82        reader: Box<dyn ReadSeek>,
83        filename: &str,
84        _encoding: Encoding,
85        archive_encoding: Encoding,
86        config: &ExtraConfig,
87        _archive: Option<&Box<dyn Script>>,
88    ) -> Result<Box<dyn Script>> {
89        Ok(Box::new(BgiArchive::new(
90            reader,
91            archive_encoding,
92            config,
93            filename,
94        )?))
95    }
96
97    fn extensions(&self) -> &'static [&'static str] {
98        &["arc"]
99    }
100
101    fn script_type(&self) -> &'static ScriptType {
102        &ScriptType::BGIArcV1
103    }
104
105    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
106        if buf_len >= 12 && buf.starts_with(b"PackFile    ") {
107            return Some(10);
108        }
109        None
110    }
111
112    fn is_archive(&self) -> bool {
113        true
114    }
115
116    fn create_archive(
117        &self,
118        filename: &str,
119        files: &[&str],
120        encoding: Encoding,
121        config: &ExtraConfig,
122    ) -> Result<Box<dyn Archive>> {
123        let f = std::fs::File::create(filename)?;
124        let writer = std::io::BufWriter::new(f);
125        Ok(Box::new(BgiArchiveWriter::new(
126            writer, files, encoding, config,
127        )?))
128    }
129}
130
131#[derive(Clone, Debug, StructPack, StructUnpack)]
132struct BgiFileHeader {
133    #[fstring = 16]
134    filename: String,
135    offset: u32,
136    size: u32,
137    #[fvec = 8]
138    _padding: Vec<u8>,
139}
140
141struct Entry<T: Read + Seek> {
142    header: BgiFileHeader,
143    reader: Arc<Mutex<T>>,
144    pos: usize,
145    base_offset: u64,
146    script_type: Option<ScriptType>,
147}
148
149impl<T: Read + Seek> ArchiveContent for Entry<T> {
150    fn name(&self) -> &str {
151        &self.header.filename
152    }
153
154    fn script_type(&self) -> Option<&ScriptType> {
155        self.script_type.as_ref()
156    }
157}
158
159impl<T: Read + Seek> Read for Entry<T> {
160    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
161        let mut reader = self.reader.lock().map_err(|e| {
162            std::io::Error::new(
163                std::io::ErrorKind::Other,
164                format!("Failed to lock mutex: {}", e),
165            )
166        })?;
167        reader.seek(SeekFrom::Start(
168            self.base_offset + self.header.offset as u64 + self.pos as u64,
169        ))?;
170        let bytes_read = buf.len().min(self.header.size as usize - self.pos);
171        if bytes_read == 0 {
172            return Ok(0);
173        }
174        let bytes_read = reader.read(&mut buf[..bytes_read])?;
175        self.pos += bytes_read;
176        Ok(bytes_read)
177    }
178}
179
180impl<T: Read + Seek> Seek for Entry<T> {
181    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
182        let new_pos = match pos {
183            SeekFrom::Start(offset) => offset as usize,
184            SeekFrom::End(offset) => {
185                if offset < 0 {
186                    if (-offset) as usize > self.header.size as usize {
187                        return Err(std::io::Error::new(
188                            std::io::ErrorKind::InvalidInput,
189                            "Seek from end exceeds file length",
190                        ));
191                    }
192                    self.header.size as usize - (-offset) as usize
193                } else {
194                    self.header.size as usize + offset as usize
195                }
196            }
197            SeekFrom::Current(offset) => {
198                if offset < 0 {
199                    if (-offset) as usize > self.pos {
200                        return Err(std::io::Error::new(
201                            std::io::ErrorKind::InvalidInput,
202                            "Seek from current exceeds current position",
203                        ));
204                    }
205                    self.pos.saturating_sub((-offset) as usize)
206                } else {
207                    self.pos + offset as usize
208                }
209            }
210        };
211        self.pos = new_pos;
212        Ok(self.pos as u64)
213    }
214
215    fn stream_position(&mut self) -> std::io::Result<u64> {
216        Ok(self.pos as u64)
217    }
218}
219
220#[derive(Debug)]
221/// Buriko General Interpreter/Ethornell Archive File Version 1 (.arc)
222pub struct BgiArchive<T: Read + Seek + std::fmt::Debug> {
223    reader: Arc<Mutex<T>>,
224    entries: Vec<BgiFileHeader>,
225    base_offset: u64,
226    #[cfg(feature = "bgi-img")]
227    is_sysgrp_arc: bool,
228}
229
230impl<T: Read + Seek + std::fmt::Debug> BgiArchive<T> {
231    /// Creates a new BGI archive from a reader.
232    ///
233    /// * `reader` - The reader to read the archive from.
234    /// * `archive_encoding` - The encoding used for the archive.
235    /// * `config` - Extra configuration options.
236    /// * `filename` - The name of the archive file.
237    pub fn new(
238        mut reader: T,
239        archive_encoding: Encoding,
240        _config: &ExtraConfig,
241        _filename: &str,
242    ) -> Result<Self> {
243        let mut header = [0u8; 12];
244        reader.read_exact(&mut header)?;
245        if !header.starts_with(b"PackFile    ") {
246            return Err(anyhow::anyhow!("Invalid BGI archive header"));
247        }
248
249        let file_count = reader.read_u32()?;
250        let mut entries = Vec::with_capacity(file_count as usize);
251        for _ in 0..file_count {
252            let entry = BgiFileHeader::unpack(&mut reader, false, archive_encoding)?;
253            entries.push(entry);
254        }
255
256        #[cfg(feature = "bgi-img")]
257        let is_sysgrp_arc = _config.bgi_is_sysgrp_arc.unwrap_or_else(|| {
258            std::path::Path::new(&_filename.to_lowercase())
259                .file_name()
260                .map(|f| f == "sysgrp.arc")
261                .unwrap_or(false)
262        });
263
264        Ok(BgiArchive {
265            reader: Arc::new(Mutex::new(reader)),
266            entries,
267            base_offset: 16 + (file_count as u64 * 32),
268            #[cfg(feature = "bgi-img")]
269            is_sysgrp_arc,
270        })
271    }
272}
273
274impl<T: Read + Seek + std::fmt::Debug + 'static> Script for BgiArchive<T> {
275    fn default_output_script_type(&self) -> OutputScriptType {
276        OutputScriptType::Json
277    }
278
279    fn default_format_type(&self) -> FormatOptions {
280        FormatOptions::None
281    }
282
283    fn is_archive(&self) -> bool {
284        true
285    }
286
287    fn iter_archive_filename<'a>(
288        &'a self,
289    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
290        Ok(Box::new(
291            self.entries.iter().map(|e| Ok(e.filename.clone())),
292        ))
293    }
294
295    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
296        Ok(Box::new(self.entries.iter().map(|e| Ok(e.offset as u64))))
297    }
298
299    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
300        if index >= self.entries.len() {
301            return Err(anyhow::anyhow!(
302                "Index out of bounds: {} (max: {})",
303                index,
304                self.entries.len()
305            ));
306        }
307        let entry = &self.entries[index];
308        let mut entry = Entry {
309            header: entry.clone(),
310            reader: self.reader.clone(),
311            pos: 0,
312            base_offset: self.base_offset,
313            script_type: None,
314        };
315        let mut buf = [0u8; 32];
316        match entry.read(&mut buf) {
317            Ok(_) => {}
318            Err(e) => {
319                return Err(anyhow::anyhow!(
320                    "Failed to read entry '{}': {}",
321                    entry.header.filename,
322                    e
323                ));
324            }
325        }
326        entry.pos = 0;
327        if buf.starts_with(b"DSC FORMAT 1.00") {
328            let data = match entry.data() {
329                Ok(data) => data,
330                Err(e) => {
331                    return Err(anyhow::anyhow!(
332                        "Failed to read DSC data for '{}': {}",
333                        entry.header.filename,
334                        e
335                    ));
336                }
337            };
338            entry.pos = 0;
339            let dsc = match DscDecoder::new(&data) {
340                Ok(dsc) => dsc,
341                Err(e) => {
342                    return Err(anyhow::anyhow!(
343                        "Failed to create DSC decoder for '{}': {}",
344                        entry.header.filename,
345                        e
346                    ));
347                }
348            };
349            let decoded = match dsc.unpack() {
350                Ok(decoded) => decoded,
351                Err(e) => {
352                    return Err(anyhow::anyhow!(
353                        "Failed to unpack DSC data for '{}': {}",
354                        entry.header.filename,
355                        e
356                    ));
357                }
358            };
359            let reader = MemReader::new(decoded);
360            if reader.data.starts_with(b"BSE 1.") {
361                match BseReader::new(reader, detect_script_type, &entry.header.filename) {
362                    Ok(bse_reader) => {
363                        return Ok(Box::new(bse_reader));
364                    }
365                    Err(e) => {
366                        return Err(anyhow::anyhow!(
367                            "Failed to create BSE reader for '{}': {}",
368                            entry.header.filename,
369                            e
370                        ));
371                    }
372                };
373            }
374            return Ok(Box::new(MemEntry {
375                name: entry.header.filename.clone(),
376                data: reader,
377                #[cfg(feature = "bgi-img")]
378                detect: if self.is_sysgrp_arc {
379                    detect_script_type_sysgrp
380                } else {
381                    detect_script_type
382                },
383                #[cfg(not(feature = "bgi-img"))]
384                detect: detect_script_type,
385            }));
386        }
387        if buf.starts_with(b"BSE 1.") {
388            let filename = entry.header.filename.clone();
389            #[cfg(feature = "bgi-img")]
390            let detect = if self.is_sysgrp_arc {
391                detect_script_type_sysgrp
392            } else {
393                detect_script_type
394            };
395            #[cfg(not(feature = "bgi-img"))]
396            let detect = detect_script_type;
397            match BseReader::new(entry, detect, &filename) {
398                Ok(mut bse_reader) => {
399                    if bse_reader.is_dsc() {
400                        let data = match bse_reader.data() {
401                            Ok(data) => data,
402                            Err(e) => {
403                                return Err(anyhow::anyhow!(
404                                    "Failed to read BSE data for '{}': {}",
405                                    &filename,
406                                    e
407                                ));
408                            }
409                        };
410                        let dsc = match DscDecoder::new(&data) {
411                            Ok(dsc) => dsc,
412                            Err(e) => {
413                                return Err(anyhow::anyhow!(
414                                    "Failed to create DSC decoder for '{}': {}",
415                                    &filename,
416                                    e
417                                ));
418                            }
419                        };
420                        let decoded = match dsc.unpack() {
421                            Ok(decoded) => decoded,
422                            Err(e) => {
423                                return Err(anyhow::anyhow!(
424                                    "Failed to unpack DSC data for '{}': {}",
425                                    &filename,
426                                    e
427                                ));
428                            }
429                        };
430                        let reader = MemReader::new(decoded);
431                        return Ok(Box::new(MemEntry {
432                            name: filename,
433                            data: reader,
434                            detect,
435                        }));
436                    }
437                    return Ok(Box::new(bse_reader));
438                }
439                Err(e) => {
440                    return Err(anyhow::anyhow!(
441                        "Failed to create BSE reader for '{}': {}",
442                        &filename,
443                        e
444                    ));
445                }
446            };
447        }
448        #[cfg(feature = "bgi-img")]
449        if self.is_sysgrp_arc {
450            entry.script_type = Some(ScriptType::BGIImg);
451        } else {
452            entry.script_type =
453                detect_script_type(&buf, buf.len(), &entry.header.filename).cloned();
454        }
455        #[cfg(not(feature = "bgi-img"))]
456        {
457            entry.script_type =
458                detect_script_type(&buf, buf.len(), &entry.header.filename).cloned();
459        }
460        Ok(Box::new(entry))
461    }
462}
463
464struct MemEntry<F: Fn(&[u8], usize, &str) -> Option<&'static ScriptType>> {
465    name: String,
466    data: MemReader,
467    detect: F,
468}
469
470impl<F: Fn(&[u8], usize, &str) -> Option<&'static ScriptType>> Read for MemEntry<F> {
471    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
472        self.data.read(buf)
473    }
474}
475
476impl<F: Fn(&[u8], usize, &str) -> Option<&'static ScriptType>> ArchiveContent for MemEntry<F> {
477    fn name(&self) -> &str {
478        &self.name
479    }
480
481    fn script_type(&self) -> Option<&ScriptType> {
482        (self.detect)(&self.data.data, self.data.data.len(), &self.name)
483    }
484
485    fn data(&mut self) -> Result<Vec<u8>> {
486        Ok(self.data.data.clone())
487    }
488
489    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + 'a>> {
490        Ok(Box::new(&mut self.data))
491    }
492}
493
494fn detect_script_type(buf: &[u8], buf_len: usize, filename: &str) -> Option<&'static ScriptType> {
495    if buf_len >= 28 && buf.starts_with(b"BurikoCompiledScriptVer1.00\0") {
496        return Some(&ScriptType::BGI);
497    }
498    #[cfg(feature = "bgi-img")]
499    if buf_len >= 16 && buf.starts_with(b"CompressedBG___") {
500        return Some(&ScriptType::BGICbg);
501    }
502    #[cfg(feature = "bgi-audio")]
503    if buf_len >= 8 && buf[4..].starts_with(b"bw  ") {
504        return Some(&ScriptType::BGIAudio);
505    }
506    let filename = filename.to_lowercase();
507    if filename.ends_with("._bp") {
508        return Some(&ScriptType::BGIBp);
509    } else if filename.ends_with("._bsi") {
510        return Some(&ScriptType::BGIBsi);
511    }
512    None
513}
514
515#[cfg(feature = "bgi-img")]
516fn detect_script_type_sysgrp(
517    _buf: &[u8],
518    _buf_len: usize,
519    _filename: &str,
520) -> Option<&'static ScriptType> {
521    Some(&ScriptType::BGIImg)
522}
523
524/// BGI Archive Writer
525pub struct BgiArchiveWriter<T: Write + Seek> {
526    writer: T,
527    headers: HashMap<String, BgiFileHeader>,
528    compress_file: bool,
529    encoding: Encoding,
530    min_len: usize,
531}
532
533impl<T: Write + Seek> BgiArchiveWriter<T> {
534    /// Creates a new BGI Archive Writer.
535    ///
536    /// * `writer` - The writer to write the archive to.
537    /// * `files` - The list of files to include in the archive.
538    /// * `encoding` - The encoding used for the archive.
539    /// * `config` - Extra configuration options.
540    pub fn new(
541        mut writer: T,
542        files: &[&str],
543        encoding: Encoding,
544        config: &ExtraConfig,
545    ) -> Result<Self> {
546        writer.write_all(b"PackFile    ")?;
547        let file_count = files.len() as u32;
548        writer.write_u32(file_count)?;
549        let mut headers = HashMap::new();
550        for file in files {
551            let header = BgiFileHeader {
552                filename: file.to_string(),
553                offset: 0,
554                size: 0,
555                _padding: vec![0; 8],
556            };
557            header.pack(&mut writer, false, encoding)?;
558            headers.insert(file.to_string(), header);
559        }
560        Ok(BgiArchiveWriter {
561            writer,
562            headers,
563            compress_file: config.bgi_compress_file,
564            encoding,
565            min_len: config.bgi_compress_min_len,
566        })
567    }
568}
569
570impl<T: Write + Seek> Archive for BgiArchiveWriter<T> {
571    fn new_file<'a>(&'a mut self, name: &str) -> Result<Box<dyn WriteSeek + 'a>> {
572        let entry = self
573            .headers
574            .get_mut(name)
575            .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
576        if entry.offset != 0 || entry.size != 0 {
577            return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
578        }
579        self.writer.seek(SeekFrom::End(0))?;
580        entry.offset = self.writer.stream_position()? as u32;
581        let file = BgiArchiveFile {
582            header: entry,
583            writer: &mut self.writer,
584            pos: 0,
585        };
586        Ok(if self.compress_file {
587            Box::new(BgiArchiveFileWithDsc::new(file, self.min_len))
588        } else {
589            Box::new(file)
590        })
591    }
592
593    fn write_header(&mut self) -> Result<()> {
594        self.writer.seek(SeekFrom::Start(0x10))?;
595        let base_offset = self.headers.len() as u32 * 0x20 + 16;
596        let mut files = self.headers.iter_mut().map(|(_, d)| d).collect::<Vec<_>>();
597        files.sort_by_key(|f| f.offset);
598        for file in files {
599            file.offset -= base_offset;
600            file.pack(&mut self.writer, false, self.encoding)?;
601        }
602        Ok(())
603    }
604}
605
606/// BGI Archive File Writer (Not compressed)
607pub struct BgiArchiveFile<'a, T: Write + Seek> {
608    header: &'a mut BgiFileHeader,
609    writer: &'a mut T,
610    pos: usize,
611}
612
613impl<'a, T: Write + Seek> Write for BgiArchiveFile<'a, T> {
614    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
615        self.writer
616            .seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?;
617        let bytes_written = self.writer.write(buf)?;
618        self.pos += bytes_written;
619        self.header.size = self.header.size.max(self.pos as u32);
620        Ok(bytes_written)
621    }
622
623    fn flush(&mut self) -> std::io::Result<()> {
624        self.writer.flush()
625    }
626}
627
628impl<'a, T: Write + Seek> Seek for BgiArchiveFile<'a, T> {
629    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
630        let new_pos = match pos {
631            SeekFrom::Start(offset) => offset as usize,
632            SeekFrom::End(offset) => {
633                if offset < 0 {
634                    if (-offset) as usize > self.header.size as usize {
635                        return Err(std::io::Error::new(
636                            std::io::ErrorKind::InvalidInput,
637                            "Seek from end exceeds file length",
638                        ));
639                    }
640                    self.header.size as usize - (-offset) as usize
641                } else {
642                    self.header.size as usize + offset as usize
643                }
644            }
645            SeekFrom::Current(offset) => {
646                if offset < 0 {
647                    if (-offset) as usize > self.pos {
648                        return Err(std::io::Error::new(
649                            std::io::ErrorKind::InvalidInput,
650                            "Seek from current exceeds current position",
651                        ));
652                    }
653                    self.pos.saturating_sub((-offset) as usize)
654                } else {
655                    self.pos + offset as usize
656                }
657            }
658        };
659        self.pos = new_pos;
660        Ok(self.pos as u64)
661    }
662}
663
664/// BGI Archive File Writer with DSC compression
665pub struct BgiArchiveFileWithDsc<'a, T: Write + Seek> {
666    writer: BgiArchiveFile<'a, T>,
667    buf: MemWriter,
668    min_len: usize,
669}
670
671impl<'a, T: Write + Seek> BgiArchiveFileWithDsc<'a, T> {
672    /// Creates a new BGI Archive File Writer with DSC compression.
673    ///
674    /// * `writer` - The writer to write the archive file to.
675    /// * `min_len` - The minimum length for LZSS compression.
676    pub fn new(writer: BgiArchiveFile<'a, T>, min_len: usize) -> Self {
677        BgiArchiveFileWithDsc {
678            writer,
679            buf: MemWriter::new(),
680            min_len,
681        }
682    }
683}
684
685impl<'a, T: Write + Seek> Write for BgiArchiveFileWithDsc<'a, T> {
686    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
687        self.buf.write(buf)
688    }
689
690    fn flush(&mut self) -> std::io::Result<()> {
691        self.buf.flush()
692    }
693}
694
695impl<'a, T: Write + Seek> Seek for BgiArchiveFileWithDsc<'a, T> {
696    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
697        self.buf.seek(pos)
698    }
699
700    fn stream_position(&mut self) -> std::io::Result<u64> {
701        self.buf.stream_position()
702    }
703
704    fn rewind(&mut self) -> std::io::Result<()> {
705        self.buf.rewind()
706    }
707}
708
709impl<'a, T: Write + Seek> Drop for BgiArchiveFileWithDsc<'a, T> {
710    fn drop(&mut self) {
711        let buf = self.buf.as_slice();
712        let encoder = DscEncoder::new(&mut self.writer, self.min_len);
713        if let Err(e) = encoder.pack(&buf) {
714            eprintln!("Failed to write DSC data: {}", e);
715            crate::COUNTER.inc_error();
716        }
717    }
718}